#!/usr/local/bin/python
# Rob Dennis - Lurchington Q. Delacroix
# Keasby - TV renamer
# Project Homepage: http://code.google.com/p/keasby/

#--- Python Imports
import unittest
import logging
import os
import shelve
import anydbm
import xml.etree.ElementTree as ET
import collections
from os.path import join as pjoin
from contextlib import closing

#--- Keasby Imports
import keasby_source
from tvdb_api import TvDbApi, TvDbApiError
from keasby_cache import KeasbyCache, KeasbyCacheError
from keasby_options import kopt
from tv_episode import TvEpisode, TvBase, TvSeries

from keasby_toolbox import log

_tvdb_cache_dir = pjoin(kopt.cl_opts.cache_dir, "tvdb")

class TvDbSourceError(keasby_source.InfoSourceError):
    """
    Generic TvDbSource Exception
    """
    pass


class TvDbSeriesId(collections.MutableMapping):
    """
    class for persistent mapping TVDB series to a given showname
    """
    def __init__(self):
        """

        """

        cache_dir = _tvdb_cache_dir

        if not os.path.exists(cache_dir):
            os.makedirs(cache_dir)

        self.open(pjoin(cache_dir, "tvdb_seriesid_map"))


    def open(self, path, mode="c"):
        """
        sets the internal persistent store to the given path
        """

        self.data_path = path
        self.map = anydbm.open(path, mode)

        log.debug("open persistent seriesid store at: %s mode: %s", path, mode)

        if log.level <= logging.DEBUG:
            for key, value in self.iteritems():
                log.debug("cache[%s] = %s", key, value)

    def close(self):
        """
        close the persistent store
        """

        log.debug("closing persistent seriesid store")
        self.map.close()

    def purge(self):
        """
        Purges cached data, requires an open() call before more writes
        """

        log.debug("purging persistent seriesid store")

        try:
            self.map.close()
        except Exception, msg:
            log.debug("failed, assuming it's already closed: %s", msg)

        try:
            os.remove(self.data_path)
        except Exception, msg:
            log.warning("unable to remove seriesid data: %s", msg)

        return self

    def __del__(self):

        self.close()

    def __contains__(self, key):

        return self._str(key) in self.map

    def __len__(self):

        return self.map.__len__()

    def __getitem__(self, key):

        return self.map.__getitem__(self._str(key))

    def __setitem__(self, key, value):

        return self.map.__setitem__(self._str(key), self._str(value))

    def __delitem__(self, key):

        return self.map.__delitem__(self._str(key))

    def __iter__(self):

        return self.map.__iter__()

    @staticmethod
    def _str(obj):
        """
        cast as string if necessary for anydbm limitation
        """

        if isinstance(obj, str):
            return obj
        elif isinstance(obj, unicode):
            return obj.encode("UTF-8")
        else:
            return str(obj)


class TvDbCache(KeasbyCache):
    """
    TvDb-specific cache implementation
    """

    def __init__(self, language):
        """
        instaniates the cache with a tvdb-specific root folder
        """

        self.language = language
        KeasbyCache.__init__(self, _tvdb_cache_dir)


    def read_zipped_sr(self, seriesid):
        """
        calls KeasbyCache.read_zip with "en.xml" as the internal path
        using the path derived from the given seriesid
        """

        self.read_zip(self.get_sr_path(seriesid),
                        "%s.xml"%self.language)

    def cache_series_zip(self, seriesid, zipfileobj):
        """
        calls KeasbyCache.cache() with the derived path and fileobj
        """

        self.cache(self.get_sr_path(seriesid), zipfileobj)

    @staticmethod
    def get_sr_path(seriesid, extension=".zip"):
        """
        return the desired cache path for the given seriesid
        """

        return pjoin("series","all","%s%s"%(str(seriesid), extension))





class TvDbSource(keasby_source.InfoSource):
    """
    This class is written for interacting with thetvdb.com's API, implementing\
    The KeasbySource protocol
    """

    api = TvDbApi("D1EBA6781E2572BB")

    def __init__(self):
        """
        Instantiate's class attributes for caching, interaction with data,\
        and for the initialization of mirrors and updates since last\
        interaction.
        """
        self.cache = TvDbCache(self.api.language)
        self.seriesid = TvDbSeriesId()


    def get_episode_title(self, tv_ep):
        """
        return episode title
        """

        assert self.prime_search(tv_ep), "unable to successfully prime search"

        seriesid, cached = self.get_seriesid(tv_ep.seriesname)

        try:
            seriesobj = self.cache.read_zipped_sr(seriesid)
            if seriesobj is None:
                log.error("Error with reading")

            element = self.api.search(seriesobj,
                                        SeasonNumber=str(tv_ep.seasonnumber),
                                        EpisodeNumber=str(tv_ep.episodenumber))


            assert len(element) == 1, "more than 1 episode found"
            log.debug("found: %s",element[0].tostring())
        finally:
            seriesobj.close()


        return element[0].findtext("EpisodeName")

    def prime_search(self, tv_base):
        """
        Acquires and caches information for later searches based on the\
        provided TvBase-derived object. return True for correctly primed\
        search.
        """

        log.debug("priming search with seriesname: %s", tv_base.seriesname)

        seriesid, cached = self.get_seriesid(tv_base.seriesname)

        log.debug("downloading resources for series id: %s (cachedL %s)",
                  seriesid, cached)

        self.cache.cache_series_zip(seriesid,
                                    self.api.open_fsr(seriesid))

        return self.cache.get_sr_path(seriesid) in self.cache

    def get_seriesid(self, seriesname, force_query=False):
        """
        Checks to see if the seriesid has already been mapped, otherwsie,
        query it. Returns a tuple of (string seriesid, bool was_cached)
        """

        if not force_query:
            log.debug("getting seriesid")
            cached_seriesid = self.seriesid.get(seriesname)
            log.debug("returned: %s", cached_seriesid)
            if cached_seriesid:
                log.debug("loading cached seriesid: %s", cached_seriesid)
                return cached_seriesid, True
        else:
            log.debug("forcing a seriesid query")

        log.debug("no seriesid cached for %s", seriesname)

        with closing(self.api.open_query_sid(seriesname)) as sidobj:
            tree = ET.parse(sidobj)
            keynames = ("seriesid", "SeriesName", "FirstAired", "Overview")
            choice_gen = (tuple(series.findtext(key) for key in keynames)
                             for series in tree.getiterator("Series"))

            log.debug("defaulted to first result at this time")
            choice = choice_gen.next()
            log.debug("chose: %s", choice)

            log.debug("saving %s to %s", choice[1], choice[0])
            self.seriesid[choice[1]] = choice[0]

        return choice[0], False

    def __str__(self):
        """
        String representation
        """
        return super(__str__(self))


class TvDbSourceTestCase(keasby_source.InfoSourceInternalTestCase):
    """
    Tests the use of the TvDbSource class
    """
    tvep = TvEpisode(seriesname=u"Lost",
                     episodetitle=u"Pilot",
                     episodenumber=1,
                     seasonnumber=1)

    test_seriesid = "73739"

    def setUp(self):
        """
        Re-initializes each run due to purge call
        """

        self.source = TvDbSource()
        self.sanity = self.source

    def tearDown(self):
        """
        completely remove cached/saved data to aid granularization
        """

        self.source.seriesid.purge()
        assert not os.path.exists(self.source.seriesid.data_path)

    def test_get_seriesid(self):
        """
        Test get_seriesid
        """

        _name = self.tvep.seriesname

        sid, cached = self.source.get_seriesid(_name)

        log.debug("testing using %s", _name)

        self.assertLog(sid == self.test_seriesid,
                        pass_message="got series id %s"%sid)

        self.assertFalse(cached)
        self.assertTrue(self.tvep.seriesname in self.source.seriesid)

        log.info("re-running to test cache")
        sid, cached = self.source.get_seriesid(_name)

        self.assertLog(sid == self.test_seriesid,
                        pass_message="got series id %s"%sid)

        self.assertTrue(cached)


    def test_prime_search(self):
        """
        Test prime_search
        """

        self.assertLog(self.source.prime_search(self.tvep),
                        fail_message="unable to prime TvDbSource",
                        pass_message="primed TvDbSource: %(expression)s")

    def test_get_episode_title(self):

        _title = self.source.get_episode_title(self.tvep)
        self.assertLog(self.tvep.episodetitle == _title,
                        pass_message="found title: %s"%_title)





if __name__ == "__main__":

    suite = unittest.TestSuite()
    suite.addTest(unittest.makeSuite(TvDbSourceTestCase))
    unittest.TextTestRunner(verbosity=2).run(suite)
